package com.nulldev.util.networking.ip.connstatus;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InterfaceAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import com.nulldev.util.VariableAPI.ArrayUtils;
import com.nulldev.util.VariableAPI.MathUtil;
import com.nulldev.util.VariableAPI.MathUtil.TimeUtil;
import com.nulldev.util.concurrency.threadIt.v4.locking.SignalBarrier;
import com.nulldev.util.logging.Logger;
import com.nulldev.util.logging.LoggerFactory;

public class NetworkStatusTracker {

	private static enum NetworkStatusChecks {
		DHCP_PRESENT_CHECK,
		DNS_QUERY_CHECK
	}

	private static class NetworkStatusTrackingThread extends Thread implements Runnable {
		private static final Logger log = LoggerFactory.getLogger();
		private final AtomicBoolean state = new AtomicBoolean();
		private final SignalBarrier sb = new SignalBarrier();
		private static final long MIN_CHECK_INTERVAL = TimeUtil.fromMinutes(1);
		private static final long MAX_CHECK_INTERVAL = TimeUtil.fromMinutes(10);
		private static final NetworkStatusChecks[] CHECKS = new NetworkStatusChecks[]
			{ NetworkStatusChecks.DHCP_PRESENT_CHECK };
		private volatile long CHECK_INTERVAL = MIN_CHECK_INTERVAL;

		public NetworkStatusTrackingThread() {
			super("nullUtil3[NST]"); /* Network Status Tracker */
		}

		@Override
		public void run() {
			while (true) {
				boolean localResult = false;
				for (final NetworkStatusChecks check : CHECKS) {
					switch (check) {
						case DHCP_PRESENT_CHECK: {
							try {
								if (isDHCPAddressPresent())
									localResult = true;
							} catch (Exception e) {
							}
							break;
						}
						case DNS_QUERY_CHECK: {
							try {
								if (isDNSWorking())
									localResult = true;
							} catch (Exception e) {
							}
							break;
						}
						default: {
							log.warn("Unknown network status check: " + check);
							break;
						}
					}
				}
				this.state.set(localResult);
				if (localResult)
					sb.signal();
				try {
					Thread.sleep(CHECK_INTERVAL);
				} catch (InterruptedException e) {
				}
				if (localResult)
					CHECK_INTERVAL = MathUtil.clamp(MIN_CHECK_INTERVAL, MAX_CHECK_INTERVAL, CHECK_INTERVAL + 1);
				else
					CHECK_INTERVAL = MathUtil.clamp(MIN_CHECK_INTERVAL, MAX_CHECK_INTERVAL, CHECK_INTERVAL - 1);
			}
		}

		public boolean isConnected() {
			return this.state.get();
		}

		public NetworkStatusTrackingThread autoStart() {
			if (!this.isAlive())
				super.start();
			return this;
		}

		public SignalBarrier barrier() {
			return this.sb;
		}
	}

	private static final NetworkStatusTrackingThread THREAD = new NetworkStatusTrackingThread();

	public static final boolean isDHCPAddressPresent() throws Exception {
		final Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
		while (interfaces.hasMoreElements()) {
			final NetworkInterface interf = interfaces.nextElement();
			if (interf.isUp() && !interf.isLoopback()) {
				final List<InterfaceAddress> adrs = interf.getInterfaceAddresses();
				for (final Iterator<InterfaceAddress> iter = adrs.iterator(); iter.hasNext();) {
					final InterfaceAddress adr = iter.next();
					final InetAddress inadr = adr.getAddress();
					if (inadr instanceof Inet4Address || inadr instanceof Inet6Address)
						return true;
				}
			}
		}
		return false;
	}

	private static final String[] DNS_LOCATIONS = new String[]
		{ "www.google.com", "www.youtube.com", "captive.apple.com" };
	private static final int DNS_QUERY_TIMEOUT = (int) TimeUtil.fromSeconds(10);

	private static boolean isDNSWorking() throws Exception {
		final InetAddress[] addresses = InetAddress.getAllByName(ArrayUtils.random(DNS_LOCATIONS));
		for (final InetAddress addr : addresses) {
			if (addr.isReachable(DNS_QUERY_TIMEOUT))
				return true;
		}
		return false;
	}

	static {
		THREAD.autoStart();
	}

	public static boolean isConnected() {
		return THREAD.autoStart().isConnected();
	}

	public static void awaitConnection() throws InterruptedException {
		if (isConnected())
			return;
		THREAD.autoStart().barrier().await();
	}

	public static void awaitConnection(final long timeout) throws InterruptedException {
		if (isConnected())
			return;
		THREAD.autoStart().barrier().awaitNanos(timeout);
	}
}
